<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8" />
    <title>软著源码整理工具</title>
		<!-- Import style -->
		<link rel="stylesheet" href="./element-plus/index.min.css" />
		<!-- Import Vue 3 -->
		<script src="./element-plus/vue.global.min.js"></script>
		<!-- Import component library -->
		<script src="./element-plus/index.full.min.js"></script>
		<script src="./element-plus/icon.index.iife.min.js"></script>
		<style>
      *{padding:0;margin:0}
			.title{text-align:right;}
      .min-btn:hover{background-color:rgba(255,255,255,0.3);}
      .el-select-dropdown{width:0px}
      .slef-dialog{background-color: unset;box-shadow: unset;}
      .slef-dialog > .el-dialog__header {display: none;}
      .slef-dialog > .el-dialog__footer {display: none;}
      .slef-dialog > .el-dialog__body{padding:0;}
		</style>
  </head>
  <body id="app" style="user-select: none">
    <div style="text-align:center;padding:20px 0;background-color:#337ecc;color:#fff;-webkit-app-region: drag">
      <div style="text-align: right;position:fixed;top:5px;height:20px;right:5px;color:#fff;z-index:2;-webkit-app-region: no-drag">
        <el-icon :size="20" @click="operationWindow('min')" class="min-btn">
          <Minus></Minus>
        </el-icon>
        <span style="padding-left:10px;"></span>
        <el-icon :size="20" @click="operationWindow('close')" class="min-btn">
          <Close></Close>
        </el-icon>
      </div>
      <div style="font-size:24px;">软著源码整理工具</div>
      <div style="font-size:12px;">v1.2.0</div>
    </div>
    <div style="height:40px"></div>
    <el-row :gutter="40" style="width:100%;font-size:12px;">
      <el-col :span="15">
        <el-row style="margin-bottom:10px;" :gutter="10" align="middle">
          <el-col :span="6" class="title"><span style="color:red;">*</span>项目名称</el-col>
          <el-col :span="18">
            <el-input v-model="formData.projectName" size="small"></el-input>
          </el-col>
        </el-row>
        <el-row style="margin-bottom:10px;" :gutter="10" align="middle">
          <el-col :span="6" class="title"><span style="color:red;">*</span>版本号</el-col>
          <el-col :span="18">
            <el-input v-model="formData.version" size="small"></el-input>
          </el-col>
        </el-row>
        <el-row style="margin-bottom:10px;" :gutter="10" align="middle">
          <el-col :span="6" class="title"><span style="color:red;">*</span>扫描路径</el-col>
          <el-col :span="18">
            <el-row :gutter="5">
              <el-col :span="18"><el-input v-model="formData.dirSrc" disabled size="small"></el-input></el-col>
              <el-col :span="6"><el-button type="primary" style="width:100%" @click="selectDirSrc" size="small">浏览</el-button></el-col>
            </el-row>
          </el-col>
        </el-row>
        <el-row style="margin-bottom:10px;" :gutter="10" align="middle">
          <el-col :span="6" class="title"><span style="color:red;">*</span>导出路径</el-col>
          <el-col :span="18">
            <el-row :gutter="5">
              <el-col :span="18"><el-input disabled v-model="formData.wordOutputSrc" size="small" placeholder="选择Word文件导出的路径"></el-input></el-col>
              <el-col :span="6"><el-button type="primary" style="width:100%" @click="exportWordSrc" size="small">浏览</el-button></el-col>
            </el-row>
          </el-col>
        </el-row>
        <el-row style="margin-bottom:10px;" :gutter="10" align="middle">
          <el-col :span="6" class="title"><span style="color:red;">*</span>扫描类型</el-col>
          <el-col :span="18">
            <el-select allow-create multiple clearable v-model="formData.fileTypeSelected" placeholder="可多选要扫描的文件类型，支持自定义类型" size="small" filterable style="width:100%">
              <el-option v-for="item in optionsData.fileType" :value="item" style="display:inline-block;">
            </el-select>
          </el-col>
        </el-row>
        <el-row style="margin-bottom:10px;" :gutter="10" align="middle">
          <el-col :span="6" class="title"><span style="color:red;">*</span>排除文件</el-col>
          <el-col :span="18">
            <el-input v-model="formData.ignoreName" size="small" placeholder="输入要排除的文件或文件夹名称，使用 “,” 隔开"></el-input>
          </el-col>
        </el-row>
        <el-row style="margin-bottom:10px;" :gutter="10" align="middle">
          <el-col :span="6" class="title">生成页数</el-col>
          <el-col :span="18">
            <el-input v-model="formData.wordPageSize" size="small" placeholder="填0或留空表示全量生成"></el-input>
          </el-col>
        </el-row>
        <el-row style="margin-bottom:10px;" :gutter="10" align="middle">
          <el-col :span="6" class="title">跳过页数</el-col>
          <el-col :span="18">
            <el-input v-model="formData.skipPage" size="small" placeholder="填0或留空表示不跳过"></el-input>
          </el-col>
        </el-row>
        <div style="padding-bottom:10px;"></div>
        <el-row style="margin-bottom:10px;" :gutter="10" align="middle">
          <el-col :span="6"></el-col>
          <el-col :span="18">
            <div style="text-align: center;">
              <el-button @click="Filing" type="primary" size="small" style="width:120px;">开始整理</el-button>
            </div>
          </el-col>
        </el-row>
        
      </el-col>
      <el-col :span="9" style="color:#5e5e5e;">
        <div style="line-height:25px;;">
          <div style="color: #000;">使用说明：</div>
          <div>1、本软件可自动扫描“扫描路径”下的所有文件。</div>
          <div>2、可对“扫描类型”、“排除文件”进行设置，以便对扫描的文件进行过滤。扫描类型支持自定义，可手动在扫描类型输入框中输入想要的文件类型，在下拉框中点击选择后即可使用。</div>
          <div>3、生成过程中会自动添加文档页眉为“项目名称”+“版本号”，页脚为当前页、共计页。</div>
          <div>4、可设定word生成后的页数。</div>
          <div>5、生成的文件存储于“导出路径”中，默认文件名为“《项目名称》 源码-当前时间戳”。</div>
        </div>
      </el-col>
    </el-row>
    <div style="position:fixed;bottom:0;left:0;right:0;height:20px;background-color:#e5e5e5;padding:0 10px;font-size:10px;line-height:20px;color:5e5e5e;">
      <span>v1.1.0</span>
      <span style="float:right;">power by ZIMING</span>
    </div>
    <!-- 赞赏 -->
    <el-dialog v-model="pageData.appreciationImg" :show-close="false" width="300" class="slef-dialog">
      <template #header></template>
      <img src="./assets/WeChat-Appreciation-Code.jpg" style="width:300px;height:300px;"/>
      <div style="margin-bottom:10px;"></div>
      <div style="text-align:center;">
        <el-icon :size="30" color="#fff" style="cursor:pointer;" @click="pageData.appreciationImg=false">
          <circle-close></circle-close>
        </el-icon>
      </div>
      <template #footer></template>
    </el-dialog>

    <el-dialog v-model="pageData.loading" :title="pageData.statusText" width="500" center :close-on-click-modal="false" :show-close="false">
      <div style="font-size:12px;">
        <div><el-progress :percentage="pageData.makePercentage" /></div>
        <div style="margin-bottom:10px;"></div>
        <div>
					已解析到{{pageData.fileCount}}个文件，{{pageData.codeLineCount}}行数据，预计生成{{Math.ceil(pageData.codeLineCount/50)}}页
					<template v-if="formData.skipPage">，已跳过{{formData.skipPage}}页。</template>
				</div>
        <div style="height:200px;">
          <div style="text-align: center;" v-if="pageData.makePercentage < 80&&pageData.makePercentage >= 25">
            <div style="padding-bottom:30px;"></div>
            <el-button type="default" @click="pageData.loading = false">取消</el-button>
          </div>
          <template v-if="pageData.makePercentage == 100">
            <div style="margin-bottom:20px;"></div>
            <div>文档生成：成功！</div>
            <div style="margin-bottom:10px;"></div>
            <div>文档位置：{{pageData.wordFileSrc}}</div>
            <div style="margin-bottom:20px;"></div>
            <el-alert type="info" :closable="false">
              <div style="line-height:1.7em;">
                感谢您的使用，本软件完全免费，如果您觉得我们的软件对您有所帮助，欢迎考虑打赏，制作不易，感谢您的支持。
                <span style="color:#337ecc;cursor: pointer;" @click="pageData.appreciationImg=true">（点击支持）</span>
              </div>
            </el-alert>
            <div style="margin-bottom:30px;"></div>
            <div style="text-align: center;">
              <el-button type="success" @click="pageData.loading = false">完成</el-button>
            </div>
          </template>
        </div>
      </div>
    </el-dialog>
  </body>
	<script>
		const { ipcRenderer } = require('electron')
		const { createApp,ref } = Vue
		const fs = require('fs')
		const path = require('path')
		const app = createApp({
			data(){
				return {
					optionsData:{
						fileType:["全部",".java",".php",".vue",".js",".ts",".css",".html",".yml",".py",".cpp",],//统计文件类型
					},
					formData:{
						projectName:'',//项目名称
						version:'V1.0.0',//版本号
						dirSrc:'',//扫描路径
						wordOutputSrc:'',//word导出路径
						ignoreName:'node_modules,.git,vender,dist,LICENSE',
						fileTypeSelected:[".conf"],
						wordPageSize:null,
						skipPage:null,
					},
          pageData:{
            codeLineCount:0,
            fileCount:0,
            makePercentage:0,
            loading:false,
            statusText:'正在扫描文件',
            wordFileSrc:'',
            appreciationImg:false,
          },
				}
			},
			methods:{
        operationWindow:function(type){
          ipcRenderer.sendSync('operation-window',type)
        },
        //选择扫描目录
				selectDirSrc:async function(){
          let dirSelected = await ipcRenderer.sendSync('file-selector',['openDirectory'],'选择项目路径')
					if(dirSelected.canceled==false){
						this.formData.dirSrc = dirSelected.filePaths[0]
					}
				},
        //选择导出路径
				exportWordSrc:async function(){
          let dirSelected = await ipcRenderer.sendSync('file-selector',['openDirectory'],'选择导出的文件路径')
					if(dirSelected.canceled==false){
						this.formData.wordOutputSrc = dirSelected.filePaths[0]
					}
				},
        //删除注释并拆分成单行数组
        deleteAnnotation:function(str){
          let reg = /\/\*([\s\S]*?)\*\//g
          let match = str.match(reg)
          if(match!=null){
            for(matchItem of match){
              str = str.replace(matchItem,"")
            }
          }
          let lines = str.split("\n");
          for (let i=0;i<lines.length;i++) {
						// 排除空行和以双斜杠开头的行
						if (lines[i].trim() == ""||lines[i].trim().startsWith("//")||lines[i].trim().startsWith("#")) {
							lines.splice(i,1)
              continue
						}
            lines[i] = lines[i].replace(/[\r\n\t]/g, '')
					}
          return lines
        },
				//获取单个文件代码行数并返回每行数据
				getLines : function (filePath) {
          return new Promise((resolve,reject)=>{
            if(this.pageData.loading==false){
              reject()
            }
            fs.readFile(filePath, "utf-8",(err,fileContent)=>{
              fileContent = this.deleteAnnotation(fileContent)
              this.pageData.codeLineCount += fileContent.length
              resolve(fileContent)
            })
          })
				},
				//遍历目录并找出代码文件
				getCodeFiles:async function (dirSrc = this.formData.dirSrc) {
          let _this = this
          let allFilePath = []
          return new Promise((resolve,reject)=>{
            if(_this.pageData.loading==false){
              return reject('用户取消操作')
            }
            fs.readdir(dirSrc,async (err,fileList)=>{
              if(err){
                _this.pageData.statusText = '目录错误'
                _this.pageData.makePercentage = 25
                return reject('目录错误')
              }
              for(let i=0;i<fileList.length;i++){
                let childFilePath = path.join(dirSrc,fileList[i])
                if(_this.fileIgnore(childFilePath)){
                  continue
                }
                let isFile = fs.statSync(childFilePath).isFile()
                if(isFile==false){
                  let nextChildFilePath
                  try {
                    nextChildFilePath = await _this.getCodeFiles(childFilePath)
                  } catch (error) {
                  }
                  allFilePath = [...allFilePath,...nextChildFilePath]
                }else{
                  if(_this.checkFileType(childFilePath)==true){
                    allFilePath.push(childFilePath)
                  }
                }
              }
              resolve(allFilePath)
            })          
          })
				},
        //检查是否为选中的文件类型
        checkFileType:function(filePath){
          if(this.formData.fileTypeSelected.length<1||this.formData.fileTypeSelected.find(item=>{return item=='全部'})){
            return true
          }
          let ext = path.extname(filePath)
          for(let item of this.formData.fileTypeSelected){
            if(item == ext){
              return true
            }
          }
          return false
        },
        //筛选排除的文件或文件夹
        fileIgnore:function(filePath){
          if(this.formData.ignoreName==false){
            return false
          }
          let igonreList = this.formData.ignoreName.split(',')
          for(let ignore of igonreList){
            if(!ignore){
              continue
            }
            if(filePath.indexOf(ignore)!=-1){
              return true
            }
          }
          return false
        },
        checkForm:function(){
          if(!this.formData.projectName) return false
          if(!this.formData.version) return false
          if(!this.formData.dirSrc) return false
          if(!this.formData.wordOutputSrc) return false
          if(this.formData.fileTypeSelected.length<1) return false
          return true
        },
        //整理代码数据
        Filing:async function(){
          if(!this.checkForm()){
            this.$alert('请将数据录入完整再试！', '系统通知')
            return
          }
          this.pageData.codeLineCount = 0,
          this.pageData.fileCount = 0,
          this.pageData.makePercentage = 0,
          this.pageData.loading = true
          this.pageData.makePercentage = 5
          let filePathList = await this.getCodeFiles()
          this.pageData.makePercentage = 25
          this.pageData.fileCount = filePathList.length
          let lineContextList = []
          this.pageData.statusText = "整理文件内容中"
          for(let i=0;i<filePathList.length;i++){
            let lineContext
            try {
              lineContext = await this.getLines(filePathList[i])
            } catch (error) {
              this.pageData.statusText = error
            }
            lineContextList = [...lineContextList,...lineContext]
            this.pageData.makePercentage = 25+parseInt(i/filePathList.length*60)
            if(this.formData.wordPageSize&&Math.ceil(this.pageData.codeLineCount/50)>=this.formData.wordPageSize){
              break
            }
          }
					if(lineContextList.length>=50*this.formData.skipPage){
						lineContextList.splice(0,50*this.formData.skipPage)
					}
          this.pageData.statusText = "Word生成中"
          this.makeWordTemplater(lineContextList)
          //this.makeWordDocx(lineContextList)
          //this.makeWordGen(lineContextList)
        },
        //最标准，但无法识别部分内容
        makeWordTemplater:function(lineContextList){
          const PizZip = require("pizzip");
          const Docxtemplater = require("docxtemplater")
					const _this = this
          const content = fs.readFileSync(
              "./assets/word-template/template1.docx",
              "binary"
          )

          const zip = new PizZip(content)

          const doc = new Docxtemplater(zip, {
              paragraphLoop: true,
          })

          this.pageData.makePercentage = 86
          try {
            doc.render({
              contentList:lineContextList,
							header:this.formData.projectName+' '+this.formData.version,
            })
          } catch (error) {
            error.properties.errors.forEach(function (err) {
              console.log(err);
              console.log(err.properties.value);
            });
						_this.makeWordGen(lineContextList)
						return
          }
					this.pageData.makePercentage = 95
          const buf = doc.getZip().generate({
              type: "nodebuffer",
              compression: "DEFLATE",
          });
          this.pageData.makePercentage = 98
          this.pageData.wordFileSrc = path.join(`${this.formData.wordOutputSrc}/《${this.formData.projectName}》 ${this.formData.version} 源码-${new Date().getTime()}.docx`)
          fs.writeFileSync(this.pageData.wordFileSrc,buf);
          this.pageData.makePercentage = 100
        },
        //无法设置文档格式，无页码，可识别特殊字符
        makeWordGen:function(lineContextList){
          const officegen = require('officegen')
          const docx = officegen({
            type:'docx',
            creator: 'admin',
          })
					let header = docx.getHeader().createP()
					header.addText(this.formData.projectName+' '+this.formData.version)
					const context = docx.createP()
          for(let i=0;i<lineContextList.length;i++){
            context.addText(lineContextList[i],{font_face: '宋体', font_size: 10.5,})//添加文本
						context.addLineBreak()
            this.pageData.makePercentage = 85+parseInt(i/lineContextList.length*10)
          }
					this.pageData.makePercentage = 98
          this.pageData.wordFileSrc = path.join(`${this.formData.wordOutputSrc}/《${this.formData.projectName}》 ${this.formData.version} 源码-${new Date().getTime()}.docx`)
          const out = fs.createWriteStream(this.pageData.wordFileSrc)
					const result = docx.generate(out)
          this.pageData.makePercentage = 100
        },
        //无法识别部分内容
				makeWordDocx:function(lineContextList){
          const { Document, Packer, Paragraph, TextRun ,Footer,PageNumber,Header } = require("docx")
          let sectionsChild = []
          for(i=0;i<lineContextList.length;i++){
            sectionsChild.push(new Paragraph({children: [new TextRun({text:lineContextList[i],size:21})]}))
            this.pageData.makePercentage+=parseInt(i/lineContextList.length*20)
          }
          let documentData = {
            sections:[{
              properties:{},
              headers: {
                default: new Header({
                  children: [new Paragraph(this.formData.projectName+' '+this.formData.version)],
                }),
              },
              children:sectionsChild,
              footers: {
                default: new Footer({
                  children: [
                    new Paragraph({
                      children: [
                          new TextRun({
                            children: ["第", PageNumber.CURRENT,"页，共",PageNumber.TOTAL_PAGES,"页"],
                          })
                      ]
                    })
                  ],
                })
              },
            }]
          }
          const doc = new Document(documentData)
          this.pageData.makePercentage = 95
          this.pageData.statusText = "写入文件"
          Packer.toBuffer(doc).then((buffer) => {
            let wordFileSrc = path.join(`${this.formData.wordOutputSrc}/《${this.formData.projectName}》 ${this.formData.version} 源码-${new Date().getTime()}.docx`)
            fs.writeFileSync(wordFileSrc, buffer);
            this.pageData.wordFileSrc = wordFileSrc
            this.pageData.makePercentage = 100
            this.pageData.statusText = "已完成"
          });
				}
			}
		})
    app.component('Minus',ElementPlusIconsVue.Minus)
    app.component('Close',ElementPlusIconsVue.Close)
    app.component('CircleClose',ElementPlusIconsVue.CircleClose)
		app.use(ElementPlus)
		app.mount('#app');
	</script>
</html>